---
description: How to manage access control rules with a Triplit schema.
---

import { Callout } from 'nextra-theme-docs';

# Authorization and access control

<Callout type="warning" emoji="⚠️">
  Access control checks run exclusively on the server, and are not enforced on
  the client. Invalid writes will only be rejected when they have been sent to
  the server.
</Callout>

Triplit provides a flexible way to define access control rules for your database, ensuring that your application data is secure without the need for complex server-side logic.

## Roles

When a client authenticates with a Triplit server and begins a session, it provides a token that contains some information about itself (see [authentication](/auth) for more information on tokens). The server will assign that token some number of roles based on the claims present in the token.

### Default roles

#### `anonymous`

The server will assign the `anonymous` role to any client that presents the `anon` token generated in the Triplit dashboard or by the Triplit CLI. You might use this token to allow unauthenticated users to access your database.

#### `authenticated`

The server will assign the `authenticated` role to any client that presents a token that has a [`sub` claim](https://mojoauth.com/glossary/jwt-subject/). The `sub` or "subject" claim is a standard JWT claim that identifies the principal user that is the subject of the JWT. Tokens with the sub claim should be issued by an authentication provider such as [Clerk](/auth/integration-guides/clerk) or [Supabase](/auth/integration-guides/clerk). Because the `sub` claim is a unique identifier, we can use it to both attribute data to a user and to restrict access to that data to them.

#### Example usage

You might use the `anonymous` role to allow unauthenticated users to read your database, but restrict inserts and updates to authenticated users. The following example allows any user to read the `todos` collection, but only authenticated users to insert or update todos:

```typescript filename="schema.ts" copy
import { Schema as S } from '@triplit/client';

export const schema = S.Collections({
  todos: {
    schema: S.Schema({
      id: S.Id(),
      text: S.String(),
      authorId: S.String(),
    }),
    permissions: {
      anonymous: {
        read: {
          filter: [true],
        },
      },
      authenticated: {
        read: {
          filter: [true],
        },
        insert: {
          filter: [['authorId', '=', '$token.sub']],
        },
        update: {
          filter: [['authorId', '=', '$token.sub']],
        },
        delete: {
          filter: [['authorId', '=', '$token.sub']],
        },
      },
    },
  },
});
```

### Custom roles

The default roles exist to make it possible to add authorization rules to your database with minimal configuration. However, your app may require more complicated role-based permission schemes than can't be modeled with only the defaults. In that case you can define your own `roles`.

Each custom role must have a name and a `match` object. When a client authenticates with a Triplit server, Triplit will check if the token matches any defined roles in the schema. If it does, the client is granted that role and will be subject to any permissions that have been defined for that it.

For example, you may author `admin` and `user` tokens with the following structure:

```typescript filename="schema.ts" copy
import { Roles } from '@triplit/client';

const roles: Roles = {
  admin: {
    match: {
      type: 'admin',
    },
  },
  user: {
    match: {
      type: 'user',
      sub: '$userId',
    },
  },
};
```

Wildcards in the `match` object (prefixed with `$`) will be assigned to [variables](/query/variables) with the prefix `$role`. For example, a JWT with the following structure would match the `user` role and assign the value `123` to the `$role.userId` variable for use in your application's permission definitions:

```typescript
// match object
{
  "type": "user",
  "sub": "$userId",
}
// Token
{
  "type": "user",
  "sub": 123
}
// Query - resolves to db.query('todos').Where('authorId', '=', 123);
db.query('todos').Where('authorId', '=', '$role.userId');
```

<Callout type="info">
  You do not need to assign a token's `sub` claim to a `$role` variable to
  reference it in a filter. You can access all of the claims on a token directly
  by using the `$token` variable prefix. e.g. `$token.sub`.
</Callout>

Your schema file should export the `roles` object for use in your schema definitions.

### Combining custom and default roles

The default roles will only be applied to tokens when your schema has not defined any custom roles. If you define a custom role, the default roles will not be applied to any tokens. If you want to reuse the default and add your own, you can do so with the `DEFAULT_ROLES` constant.

```typescript filename="schema.ts" copy
import { DEFAULT_ROLES, type Roles, Schema as S } from '@triplit/client';
const roles: Roles = {
  ...DEFAULT_ROLES,
  admin: {
    match: {
      type: 'admin',
    },
  },
  user: {
    match: {
      type: 'user',
      sub: '$userId',
    },
  },
};
```

## Permissions

<Callout type="warning">
  Access control at the attribute level is not yet supported, but will be in a
  future release.
</Callout>

By default, there are no access controls on the database and they must be configured by adding a `permissions` definition to the schema. Each collection in a schema can have a `permissions` object that defines the access control rules for that collection. Once a permissions object is defined, Triplit will enforce the provided rules for each operation on the collection. If no rules for an operation are provided, the operation not be allowed by default.

The following example turns off all access to the `todos` collection so it is only accessible with your [`service` token](/auth#tokens):

```typescript filename="schema.ts" copy
import { type Roles, Schema as S } from '@triplit/client';

const roles: Roles = {
  // Role definitions
};

const schema = S.Collections({
  todos: {
    schema: S.Schema({
      id: S.Id(),
      text: S.String(),
      authorId: S.String(),
    }),
    permissions: {},
  },
});

export { schema, roles };
```

Collection permissions are defined for each operation and role. If a role is not included, it will not be allowed to perform that operation. When performing each operation, Triplit will check the set of set of [filter clauses](/query/where) that must be satisfied for the operation to be allowed.

```json
{
   "role": {
      "operation": {
         "filter": // Boolean filter expression
      }
   }
}
```

### Read

To allow clients to read data, you must define a `read` permission that specifies the roles that may read data and any additional restrictions. The following example allows a `user` to read the todos that they authored and an `admin` to read any todo:

```typescript filename="schema.ts" copy
import { type Roles, Schema as S } from '@triplit/client';

const roles: Roles = {
  // Role definitions
};

const schema = S.Collections({
  todos: {
    schema: S.Schema({
      id: S.Id(),
      text: S.String(),
      authorId: S.String(),
    }),
    permissions: {
      admin: {
        read: {
          // Allow all reads
          filter: [true],
        },
      },
      user: {
        read: {
          // Allow reads where authorId is the user's id
          filter: [['authorId', '=', '$role.userId']],
        },
      },
    },
  },
});

export { schema, roles };
```

### Insert

To allow clients to insert data, you must define an `insert` permission that specifies the roles that may insert data and any additional restrictions. The following example allows a `user` to insert a todo that they author and an `admin` to insert any todo:

```typescript filename="schema.ts" copy
import { type Roles, Schema as S } from '@triplit/client';

const roles: Roles = {
  // Custom role definitions
};

const schema = S.Collections({
  todos: {
    schema: S.Schema({
      id: S.Id(),
      text: S.String(),
      authorId: S.String(),
    }),
    permissions: {
      admin: {
        insert: {
          // Allow all inserts
          filter: [true],
        },
      },
      user: {
        insert: {
          // Allow inserts where authorId is the user's id
          filter: [['authorId', '=', '$role.userId']],
        },
      },
    },
  },
});

export { schema, roles };
```

### Update

To allow users to update data, you must define an `update` permission that specifies the roles that may update data and any additional restrictions. For updates, the permission is checked against the "old" state of the entity, before it has been updated. The following example allows a `user` to update todos that they authored and an `admin` to update any todo:

```typescript filename="schema.ts" copy
import { type Roles, Schema as S } from '@triplit/client';

const roles = {
  // Custom role definitions
};

const schema = S.Collections({
  todos: {
    schema: S.Schema({
      id: S.Id(),
      text: S.String(),
      authorId: S.String(),
    }),
    permissions: {
      admin: {
        update: {
          // Allow all updates
          filter: [true],
        },
      },
      user: {
        update: {
          // Allow updates where authorId is the user's id
          filter: [['authorId', '=', '$role.userId']],
        },
      },
    },
  },
});

export { schema, roles };
```

### Post update

You may also optionally define a `postUpdate` permission that will be run after an update operation has been completed. This is useful for confirming that updated data is valid. For example, this checks that a `user` has not re-assigned a todo to another `user`:

```typescript filename="schema.ts" copy
import { type Roles, Schema as S } from '@triplit/client';

const roles: Roles = {
  // Custom role definitions
};

const schema = S.Collections({
  todos: {
    schema: S.Schema({
      id: S.Id(),
      text: S.String(),
      authorId: S.String(),
    }),
    permissions: {
      user: {
        update: {
          // Allow updates where authorId is the user's id
          filter: [['authorId', '=', '$role.userId']],
        },
        postUpdate: {
          // Check that the authorId has not changed
          filter: [['authorId', '=', '$role.userId']],
        },
      },
    },
  },
});

export { schema, roles };
```

#### `$prev` variable

The `postUpdate` permission has access to the previous value of the document via the `$prev` variable. This allows you to check that the new value is valid in the context of the old value, e.g. that the previous value was unchanged:

```typescript filename="schema.ts" copy
import { type Roles, Schema as S } from '@triplit/client';
const roles: Roles = {
  // Custom role definitions
};
const schema = S.Collections({
  todos: {
    schema: S.Schema({
      id: S.Id(),
      text: S.String(),
      authorId: S.String(),
      updatedAt: S.Date(),
      completed: S.Boolean(),
    }),
    permissions: {
      user: {
        read: {
          filter: [true],
        },
        insert: {
          filter: [true],
        },
        update: {
          filter: [['authorId', '=', '$role.userId']],
        },
        postUpdate: {
          // Check that the authorId has not changed
          // and that the updatedAt date is greater than the previous value
          filter: [
            ['authorId', '=', '$prev.authorId'],
            ['updatedAt', '>', '$prev.updatedAt'],
          ],
        },
      },
    },
  },
});
```

### Delete

To allow users to delete data, you must define a `delete` permission that specifies the roles that may delete data and any additional restrictions. The following example allows a `user` to delete todos that they authored and an `admin` to delete any todo:

```typescript
import { type Roles, Schema as S } from '@triplit/client';

// schema.ts
const roles: Roles = {
  // Custom role definitions
};

const schema = S.Collections({
  todos: {
    schema: S.Schema({
      id: S.Id(),
      text: S.String(),
      authorId: S.String(),
    }),
    permissions: {
      admin: {
        delete: {
          // Allow all deletes
          filter: [true],
        },
      },
      user: {
        delete: {
          // Allow deletes where authorId is the user's id
          filter: [['authorId', '=', '$role.userId']],
        },
      },
    },
  },
});

export { schema, roles };
```

## Editing permissions

Permissions are a part of your schema and can be added or updated by [modifying your schema](/schemas/updating). In a future release, you will be able to manage permissions in your project's [Dashboard](https://www.triplit.dev/dashboard).

## Modeling permissions with external authentication

When using an external authentication provider like [Clerk](/auth/integration-guides/clerk), the provider is the source of truth for identifying users. This means that in your Triplit database you might not need a traditional users collection. Permissions that restrict access to specific authenticated users should use the ids provided by the auth service. If you want to store additional information about a user in Triplit, we recommend using a `profiles` collection that uses the same ID as the user ID provided from your auth provider. When your app loads and a user authenticates, you can fetch their profile or create it if it doesn't exist. Here’s an example schema:

```typescript filename="schema.ts" copy
import { type Roles, Schema } from '@triplit/client';

const roles: Roles = {
  user: {
    match: {
      sub: '$userId',
    },
  },
};

const schema = S.Collections({
  profiles: {
    schema: S.Schema({
      id: S.Id(), // Use the user ID from your auth provider when inserting
      nickname: S.String(),
      created_at: S.Date({ default: S.Default.now() }),
    }),
    permissions: {
      user: {
        read: {
          filter: [['id', '=', '$role.userId']],
        },
        update: {
          filter: [['id', '=', '$role.userId']],
        },
        insert: {
          filter: [['id', '=', '$role.userId']],
        },
      },
    },
  },
  todos: {
    schema: S.Schema({
      id: S.Id(),
      text: S.String(),
      authorId: S.String(),
    }),
    permissions: {
      user: {
        read: {
          filter: [['authorId', '=', '$role.userId']],
        },
        insert: {
          filter: [['authorId', '=', '$role.userId']],
        },
        update: {
          filter: [['authorId', '=', '$role.userId']],
        },
        delete: {
          filter: [['authorId', '=', '$role.userId']],
        },
      },
    },
  },
});
```

## Modeling selective public access

Sometimes you may want to allow a user to share a link to a resource that is not publicly accessible. For example, you have a table `documents` and only the author can read their own documents.

```typescript
const schema = S.Collections({
  documents: {
    schema: S.Schema({
      id: S.Id(),
      title: S.String(),
      content: S.String(),
      authorId: S.String(),
    }),
    permissions: {
      authenticated: {
        read: {
          filter: [
            // Only the author can read their own documents
            ['authorId', '=', '$role.userId'],
          ],
        },
      },
    },
  },
});
```

To allow selective public access, you can use the `or` function to add another filter with a [`$query` variable](/query/variables#query-variables), allowing the requesting user to read the document if they know the id.

```typescript {13-18}
const schema = S.Collections({
  documents: {
    schema: S.Schema({
      id: S.Id(),
      title: S.String(),
      content: S.String(),
      authorId: S.String(),
    }),
    permissions: {
      authenticated: {
        read: {
          filter: [
            or([
              // Only the author can read their own documents
              ['authorId', '=', '$role.userId'],
              // Anyone can read the document if they know the id
              ['id', '=', '$query.docId'],
            ]),
          ],
        },
      },
    },
  },
});
```

A client requesting the document can use the `Vars` method on the query builder to pass in the `docId` variable to the query:

```typescript
const query = client
  .query('documents')
  .Vars({ docId: '1234' }) // Allows access to the document with id 1234
  .Where('id', '=', '1234'); // Filters to just the document with id 1234

const document = await client.fetch(query);
```

Now you can implement a shareable link like `https://myapp.com/share/1234` and use that id (`1234`) to fetch a document as needed!
